//============================================================================
//      Nombre : TpGrupo8.cpp
//       Autor : Grupo8
//     Versión : 1.0
// Descripción : Trabajo Práctico de Organización de Datos (75.06) del Grupo 8
//               Compresor y descompresor por bloques
//               BWT biyectivo + MTF + Huffman dinámico
//============================================================================

#include <string>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <sys/time.h>
#include "./bwt.h"
#include "./bwtBiyectivo.h"
#include "./huffman.h"
#include "./mtf.h"

#include <iostream>

#define MAIN_CORRECTO 0
#define MAIN_ERROR -1
#define CORRECTO 3
#define ERROR -3
#define BLOQUE_TAMANYO 524288

struct structArchivos {
		std::string archivoEntradaNombre, archivoSalidaNombre;
		FILE *archivoEntradaHandler, *archivoSalidaHandler;
		long int archivoEntradaTamanyo;
};

enum tipoDeTrabajo {
	COMPRESION = 3, DESCOMPRESION = 4
};

// el bloque contiene los caracteres a codificar/comprimir en cada ciclo
unsigned char bloque[BLOQUE_TAMANYO];  // Necesario que sea global porque lo usa la funcion de comparación del qsort().
unsigned int ciclos[BLOQUE_TAMANYO];  // Necesario que sea global porque lo usa la funcion de comparación del qsort().

int procesarArgumentosPorLineaDeComando(int const argc, char const * const * const argv, structArchivos * const archivos, tipoDeTrabajo * const trabajo);
int abrirArchivos(structArchivos * const archivos);
int medirArchivoDeEntrada(structArchivos * const archivos);
int cerrarArchivos(structArchivos * const archivos);
void inicializar(Tipo_MTFTabla * const mTFTabla, struct_Huffman * const huffman);
void resultados_compresion(struct structArchivos const * const archivos, struct timeval inicio, struct timeval fin);
void resultados_descompresion(struct structArchivos const * const archivos, struct timeval inicio, struct timeval fin);

int main(int const argc, char const * const * const argv) {
	struct timeval inicio, fin;  // para medicion tiempo
	::gettimeofday(&inicio, NULL);  // tiempo inicial

	struct structArchivos archivos;
	tipoDeTrabajo trabajo;

	if (procesarArgumentosPorLineaDeComando(argc, argv, &archivos, &trabajo) != CORRECTO) {
		return MAIN_ERROR;
	}
	if (abrirArchivos(&archivos) != CORRECTO) {
		return MAIN_ERROR;
	}
	if (medirArchivoDeEntrada(&archivos) != CORRECTO) {
		cerrarArchivos(&archivos);
		return MAIN_ERROR;
	}

	unsigned long int faltaProcesar;  // longitud que resta procesar del archivo
	unsigned int longitudAProcesar;  // longitud del bloque
	Tipo_MTFTabla mTFTabla;
	struct_Huffman huffman;

	switch (trabajo) {
		case COMPRESION:
			inicializar(&mTFTabla, &huffman);  // inicializar proceso
			::fwrite(&archivos.archivoEntradaTamanyo, sizeof archivos.archivoEntradaTamanyo, 1, archivos.archivoSalidaHandler);  // escribir largo en archivo salida
			faltaProcesar = archivos.archivoEntradaTamanyo;  // para ciclar

			// ciclo por bloques: bwt -> mtf -> huf
			while (faltaProcesar) {
				if (faltaProcesar >= BLOQUE_TAMANYO) {
					longitudAProcesar = BLOQUE_TAMANYO;
				} else {
					longitudAProcesar = faltaProcesar;
				}

				// carga un bloque desde la entrada
				for (unsigned int contador = 0; contador < longitudAProcesar; ++contador) {
					bloque[contador] = ::fgetc(archivos.archivoEntradaHandler);
				}
				// TODO (Iván): ¿Por qué no mejor:?
				//::fread(bloque, sizeof (unsigned char), longitudAProcesar, archivos.archivoEntradaHandler);

				// transforma el bloque por BWT
				bwtBiyectivo_codificar(bloque, longitudAProcesar);

				// transforma el bloque por MTF
				mtf_codificar(&mTFTabla, bloque, longitudAProcesar);

				// comprime el bloque por Huffman y escribe a la salida
				huffman_comprimir(&huffman, archivos.archivoSalidaHandler, bloque, longitudAProcesar);

				// resta la longitud del bloque procesado
				faltaProcesar -= longitudAProcesar;
			}
			huffman_finalizar(&huffman, archivos.archivoSalidaHandler);

			::gettimeofday(&fin, NULL);  // tiempo final
			resultados_compresion(&archivos, inicio, fin);  // mostrar resultados compresion
			break;

		case DESCOMPRESION:
			inicializar(&mTFTabla, &huffman);  // inicializar proceso
			long int archivoSalidaTamanyo;
			::fread(&archivoSalidaTamanyo, sizeof archivoSalidaTamanyo, 1, archivos.archivoEntradaHandler);  // leer largo guardado en el archivo
			faltaProcesar = archivoSalidaTamanyo;  // para ciclar

			// ciclo por bloques: huf -> mtf -> bwt
			while (faltaProcesar) {
				if (faltaProcesar >= BLOQUE_TAMANYO) {
					longitudAProcesar = BLOQUE_TAMANYO;
				} else {
					longitudAProcesar = faltaProcesar;
				}

				// lee de la entrada y descomprime un bloque por Huffman
				huffman_descomprimir(&huffman, archivos.archivoEntradaHandler, bloque, longitudAProcesar);

				// transforma inversamente el bloque por MTF
				mtf_decodificar(&mTFTabla, bloque, longitudAProcesar);

				// transforma inversamente el bloque por BWT
				bwtBiyectivo_decodificar(bloque, longitudAProcesar);

				// escribe un bloque a la salida
				for (unsigned int contador = 0; contador < longitudAProcesar; ++contador) {
					fputc(bloque[contador], archivos.archivoSalidaHandler);
				}
				// TODO (Iván): ¿Por qué no mejor:?
				//::fwrite(bloque, sizeof (unsigned char), longitudAProcesar, archivos.archivoSalidaHandler);

				// resta la longitud del bloque procesado
				faltaProcesar -= longitudAProcesar;
			}
			::gettimeofday(&fin, NULL);  // tiempo final
			resultados_descompresion(&archivos, inicio, fin);  // mostrar resultados descompresion
			break;

		default:

			break;
	}

	if (cerrarArchivos(&archivos) != CORRECTO) {
		huffman_destruir(&huffman);  // liberar memoria
		return MAIN_ERROR;
	}
	huffman_destruir(&huffman);  // liberar memoria
	return MAIN_CORRECTO;
}

int procesarArgumentosPorLineaDeComando(int const argc, char const * const * const argv, structArchivos * const archivos, tipoDeTrabajo * const trabajo) {
	if (argc != 3) {
		::printf("ERROR: Use la sintaxis: TpGrupo8 -<opcion> <ruta al archivo de entrada>\n");
		return ERROR;
	} else {
		// armar nombre del archivo de salida
		if (::strcmp(argv[1], "-c") == 0) {
			archivos->archivoEntradaNombre = argv[2];  // obtener nombre del archivo de entrada
			archivos->archivoSalidaNombre = archivos->archivoEntradaNombre + ".8";
			*trabajo = COMPRESION;
			return CORRECTO;
		} else {
			if (::strcmp(argv[1], "-d") == 0) {
				archivos->archivoEntradaNombre = argv[2];  // obtener nombre del archivo de entrada
				archivos->archivoSalidaNombre = archivos->archivoEntradaNombre + ".d";
				*trabajo = DESCOMPRESION;
				return CORRECTO;
			} else {
				::printf("\nError: opciones: -c (comprimir), -d (descomprimir)\n");
				return ERROR;
			}
		}
	}
}

int abrirArchivos(structArchivos * const archivos) {
	archivos->archivoEntradaHandler = ::fopen(archivos->archivoEntradaNombre.c_str(), "rb");
	if (archivos->archivoEntradaHandler == NULL) {
		::printf("\nError al abrir archivo de entrada: %s", archivos->archivoEntradaNombre.c_str());
		return ERROR;
	}
	archivos->archivoSalidaHandler = ::fopen(archivos->archivoSalidaNombre.c_str(), "wb");
	if (archivos->archivoSalidaHandler == NULL) {
		::printf("\nError al abrir archivo de salida : %s", archivos->archivoSalidaNombre.c_str());
		::fclose(archivos->archivoEntradaHandler);
		return ERROR;
	}
	return CORRECTO;
}

int medirArchivoDeEntrada(structArchivos * const archivos) {
	::fseek(archivos->archivoEntradaHandler, 0, SEEK_END);
	archivos->archivoEntradaTamanyo = ::ftell(archivos->archivoEntradaHandler);
	::rewind(archivos->archivoEntradaHandler);
	return CORRECTO;
}

int cerrarArchivos(structArchivos * const archivos) {
	if (::fclose(archivos->archivoEntradaHandler) != 0) {
		::printf("\nError al cerrar el archivo de entrada: %s", archivos->archivoEntradaNombre.c_str());
		if (::fclose(archivos->archivoSalidaHandler) != 0) {
			::printf("\nError al cerrar el archivo de salida: %s", archivos->archivoSalidaNombre.c_str());
		}
		return ERROR;
	}
	if (::fclose(archivos->archivoSalidaHandler) != 0) {
		::printf("\nError al cerrar el archivo de salida: %s", archivos->archivoSalidaNombre.c_str());
		return ERROR;
	}
	return CORRECTO;
}

// inicializacion del proceso
void inicializar(Tipo_MTFTabla * const mTFTabla, struct_Huffman * const huffman) {
	mtf_inicializar(mTFTabla);  // inicializar tabla para MTF
	huffman_inicializar(huffman);  // inicializar arbol de Huffman
}

void resultados_compresion(struct structArchivos const * const archivos, struct timeval inicio, struct timeval fin) {
	long duracion;  // para medicion tiempo
	float tasa;
	float bpb;

	// obtiene largo del archivo de archivo_salida
	long int archivoSalidaTamanyo = ::ftell(archivos->archivoSalidaHandler);

	// calcula tasa de compresion
	tasa = (((float) archivos->archivoEntradaTamanyo - (float) archivoSalidaTamanyo) / (float) archivos->archivoEntradaTamanyo) * 100;

	// calcula bits/byte
	bpb = ((float) archivoSalidaTamanyo / (float) archivos->archivoEntradaTamanyo) * 8;

	// calcula duracion
	duracion = (fin.tv_sec - inicio.tv_sec) * 1000000;
	duracion += fin.tv_usec - inicio.tv_usec;

	// Presentacion resultados
	::printf("Compresion\n");
	::printf("     Archivo original: %s\n", archivos->archivoEntradaNombre.c_str());
	::printf("   Archivo comprimido: %s\n", archivos->archivoSalidaNombre.c_str());
	::printf("       Largo original: %7li bytes\n", archivos->archivoEntradaTamanyo);
	::printf("     Largo comprimido: %7li bytes\n", archivoSalidaTamanyo);
	::printf("      Tasa compresion: %5.2f %%\n", tasa);
	::printf("       Tasa bits/byte: %4.2f bpb\n", bpb);
	::printf("     Tiempo utilizado: %.3f segs\n", duracion / 1000000.0);
}

void resultados_descompresion(struct structArchivos const * const archivos, struct timeval inicio, struct timeval fin) {
	long duracion;  // para medicion tiempo
	// obtiene largo del archivo de archivo_salida
	long int archivoSalidaTamanyo = ::ftell(archivos->archivoSalidaHandler);

	// calcula duracion
	duracion = (fin.tv_sec - inicio.tv_sec) * 1000000;
	duracion += fin.tv_usec - inicio.tv_usec;

	// Presentacion resultados
	::printf("Descompresion\n");
	::printf("   Archivo comprimido: %s\n", archivos->archivoEntradaNombre.c_str());
	::printf("Archivo descomprimido: %s\n", archivos->archivoSalidaNombre.c_str());
	::printf("     Largo comprimido: %7li bytes\n", archivos->archivoEntradaTamanyo);
	::printf("  Largo descomprimido: %7li bytes\n", archivoSalidaTamanyo);
	::printf("     Tiempo utilizado: %.3f segs\n", duracion / 1000000.0);
}

